package umage.action;

import java.awt.event.ActionEvent;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.JFileChooser;
import javax.swing.JOptionPane;

import org.jhotdraw.draw.Drawing;
import org.jhotdraw.draw.Figure;
import org.jhotdraw.draw.LineConnectionFigure;

import umage.antlr.ClassUnit;
import umage.antlr.FieldUnit;
import umage.antlr.InterfaceUnit;
import umage.antlr.JavaUnit;
import umage.antlr.JavaUnit.Modifier;
import umage.antlr.MethodUnit;
import umage.core.UMageApplication;
import umage.figures.AssociationArrow;
import umage.figures.ClassFigure;
import umage.figures.CompositionArrow;
import umage.figures.DependencyArrow;
import umage.figures.GeneralizationArrow;
import umage.figures.ImplementsArrow;
import umage.figures.InterfaceFigure;
import umage.projects.ClassDiagramProject;

@ActionInfo(name = "Import", menu = "Code")
public class ClassImportAction extends DefaultAction {
	protected JFileChooser fileChooser;
	
	// state data
	protected Drawing drawing;
	protected HashMap<String, InterfaceUnit> units;
	protected HashMap<String, ClassFigure> figures;
	
	public ClassImportAction() {
		fileChooser = new JFileChooser(System.getProperty("user.home"));
		fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		fileChooser.setMultiSelectionEnabled(true);
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		
		final UMageApplication app = getApplication();
		if (!(app.getCurrentProject() instanceof ClassDiagramProject)) return;
		
		// Ask the user what to import and walk any directories they give
		if (fileChooser.showOpenDialog(app.getComponent()) != JFileChooser.APPROVE_OPTION) return;
		File[] selectedFiles = fileChooser.getSelectedFiles();
		final ArrayList<File> files = new ArrayList<File>();
		for (File file : selectedFiles) {
			Path path = file.toPath();
			if (Files.isDirectory(path)) {
				try {
					Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
						@Override
						public FileVisitResult visitFile(Path file,
								BasicFileAttributes attrs)
								throws IOException {
							if (file.toFile().getPath().endsWith(".java")) {
								files.add(file.toFile());
							}
							return super.visitFile(file, attrs);
						}
					});
				} catch (IOException e1) {}
			} else {
				if (file.getPath().endsWith(".java")) {
					files.add(file);
				}
			}
		}
		
		// Parse all files into a HashMap
		units = new HashMap<String, InterfaceUnit>();
		PrintStream defaultErr = System.err;
		ByteArrayOutputStream stringStream = new ByteArrayOutputStream();
		PrintStream errorIntercept = new PrintStream(stringStream);
		StringBuilder errorString = new StringBuilder();
		try {
			System.setErr(errorIntercept);
			for (File file : files) {
				stringStream.reset();
				InterfaceUnit unit = JavaUnit.parseUnit(file);
				if (stringStream.size() > 0) {
					errorString.append(file.getPath());
					errorString.append('\n');
					errorString.append(stringStream.toString());
					unit = null;
				}
				if (unit == null) continue;
				units.put(unit.getName(), unit);
			}
		} finally {
			System.setErr(defaultErr);
		}
		
		// Begin creating the map of all class figures
		drawing = ((ClassDiagramProject)app.getCurrentProject()).getEditor().getView().getDrawing();
		figures = new HashMap<String, ClassFigure>();
		for (Figure figure : drawing.getFigures()) {
			if (figure instanceof ClassFigure) {
				ClassFigure cFigure = (ClassFigure) figure;
				figures.put(cFigure.getName(), cFigure);
			}
		}
		
		for (InterfaceUnit unit : units.values()) {
			bindUnit(unit, "");
		}
		
		for (InterfaceUnit unit : units.values()) {
			connectUnit(unit, "");
		}
		
		units = null;
		drawing = null;
		figures = null;
		
		if (errorString.length() > 0) {
			JOptionPane.showMessageDialog(null, "Errors occured during parsing:\n" + errorString.toString(),
					"Parsing Error", JOptionPane.ERROR_MESSAGE);
		}
	}
	
	// Recursive binding algorithm
	protected void bindUnit(InterfaceUnit unit, String qualifier) {
		String qualifiedName = qualifier + unit.getName();
		ClassFigure figure = figures.remove(qualifiedName);
		boolean notAdded = (figure == null);
		if (figure == null) {
			if (unit instanceof ClassUnit) {
				figure = new ClassFigure();
			} else { // unit instanceof InterfaceUnit
				figure = new InterfaceFigure();
			}
		} else if (unit instanceof ClassUnit && figure instanceof InterfaceFigure) return;
		
		figure.setName(qualifiedName);
		figure.setAbstract(unit.getModifiers().contains(Modifier.ABSTRACT));
		for (FieldUnit field : unit.getFields()) {
			String fieldName = formatField(field, false);
			figure.addField(fieldName);
		}
		for (MethodUnit method : unit.getMethods()) {
			String methodName = formatMethod(method);
			figure.addMethod(methodName);
			figure.setAbstract(methodName, method.getModifiers().contains(Modifier.ABSTRACT));
		}
		
		if (notAdded) drawing.add(figure);
		figures.put(qualifiedName, figure);
		
		for (InterfaceUnit innerUnit : unit.getInnerUnits()) {
			bindUnit(innerUnit, qualifiedName + '.');
		}
		
	}
	
	protected ClassFigure connectUnit(InterfaceUnit unit, String qualifier) {
		String qualifiedName = qualifier + unit.getName();
		ClassFigure figure = figures.get(qualifiedName);
		if (figure == null) return null;
		
		Set<String> referencedClasses = new HashSet<String>(unit.getReferencedUnitNames());
		
		// Remove all lines starting at this figure to allow for proper connection importing and prevent overlapping
		for(Figure pFig : new ArrayList<Figure>(drawing.getFigures())) {
			if (pFig instanceof LineConnectionFigure) {
				LineConnectionFigure cFig = (LineConnectionFigure) pFig;
				if (cFig.getStartFigure() == figure) drawing.remove(cFig);
			}
		}
		
		// Draw superclass arrow
		if (unit instanceof ClassUnit) {
			String superClassName = ((ClassUnit)unit).getSuperClassName();
			if (superClassName != null) {
				referencedClasses.remove(superClassName);
				ClassFigure superFigure = figures.get(superClassName);
				if (superFigure != null) createConnection(new GeneralizationArrow(), figure, superFigure);
			}
		}
		
		// Draw interface arrow
		for (String intName : unit.getInterfaceNames()) {
			referencedClasses.remove(intName);
			ClassFigure intFigure = figures.get(intName);
			if (intFigure != null) createConnection(new ImplementsArrow(), figure, intFigure);
		}
		
		// Draw composition arrows
		for (FieldUnit field : unit.getFields()) {
			String fieldType = field.getType();
			referencedClasses.remove(fieldType);
			ClassFigure fieldFigure = figures.get(fieldType);
			if (fieldFigure != null) createConnection(new CompositionArrow(), figure, fieldFigure);
		}
		
		// Do all of this for inner classes as well
		for (InterfaceUnit childUnit : unit.getInnerUnits()) {
			ClassFigure innerFigure = connectUnit(childUnit, qualifiedName + '.');
			referencedClasses.remove(innerFigure.getName());
			referencedClasses.remove(childUnit.getName());
			if (innerFigure != null) createConnection(new DependencyArrow(), innerFigure, figure);
		}
		
		// Draw relation arrows
		for (String refName : referencedClasses) {
			ClassFigure refFigure = figures.get(refName);
			if (refFigure != null) createConnection(new AssociationArrow(), figure, refFigure);
		}
		
		return figure;
		
	}
	
	protected String formatField(FieldUnit field, boolean asPara) {
		StringBuilder builder = new StringBuilder();
		if (!asPara) {
			EnumSet<Modifier> mods = field.getModifiers();
			if (mods.contains(Modifier.PUBLIC)) {
				builder.append('+');
			} else if (mods.contains(Modifier.PRIVATE)) {
				builder.append('-');
			} else if (mods.contains(Modifier.PROTECTED)) {
				builder.append('#');
			} else {
				builder.append('~');
			}
			if (mods.contains(Modifier.STATIC)) {
				builder.append('_');
			}
			builder.append(' ');
		}
		builder.append(field.getName());
		builder.append(" : ");
		builder.append(field.getType());
		return builder.toString();
	}
	
	protected String formatMethod(MethodUnit method) {
		StringBuilder builder = new StringBuilder();
		EnumSet<Modifier> mods = method.getModifiers();
		if (mods.contains(Modifier.PUBLIC)) {
			builder.append('+');
		} else if (mods.contains(Modifier.PRIVATE)) {
			builder.append('-');
		} else if (mods.contains(Modifier.PROTECTED)) {
			builder.append('#');
		} else {
			builder.append('~');
		}
		if (mods.contains(Modifier.STATIC)) {
			builder.append('_');
		}
		builder.append(' ');
		builder.append(method.getName());
		builder.append('(');
		List<FieldUnit> paras = method.getParameters();
		for (int i = 0; i < paras.size(); i++) {
			builder.append(formatField(paras.get(i), true));
			if (i != paras.size() - 1) builder.append(", ");
		}
		builder.append(')');
		builder.append(" : ");
		if (method.getReturnType() != null) builder.append(method.getReturnType());
		else builder.append("void");
		
		return builder.toString();
	}
	
	protected void createConnection(LineConnectionFigure arrow, Figure from, Figure to) {
		arrow.setStartConnector(from.findConnector(from.getStartPoint(), arrow));
		arrow.setEndConnector(to.findConnector(to.getStartPoint(), arrow));
		arrow.updateConnection();
		drawing.add(arrow);
	}

}
